using System;
using System.Collections;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using Skyboxes;
using ZoomEngine.Animation;

namespace tutFuelCellGame
{

    public enum GameState { Loading, StartCountDown, CountDown, Running, Won, Lost }

    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class FuelCellGame : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        GameObject ground;
        Camera gameCamera;
        Random random;
        FuelCarrier fuelCarrier;
        FuelCells fuelCells;
        Barriers barriers;
        InputManager inputManager;
        SoundManager soundManager;
        AnimationManager animationManager;
        Skybox skybox;
        SpriteFont font;
        SpriteFont countdownfont;
        BarrierAnimationData barrierAnimationData;
        Levels levels;
        GameState currentGameState = GameState.Loading;
        GameState previousGameState = GameState.Loading;

        ClockManager clockManager;


        // gamestate trackers
        private int retrievedFuelCells, oldRetrievedFuelCells;
        TimeSpan roundTimer;
        float FPS;

        private Model boundingSphereModel;

        public LightSource OrangeLightSource;
        public LightSource BlueLightSource;
        private Plane shadowPlane;
        private Matrix shadow;
        private bool GameIsRunningFast = true;
        public Model[] barrierModels = new Model[3];
        private LevelCountDown countDown;
        private Hud hud;

        public FuelCellGame()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            roundTimer = TimeSpan.Zero;

            clockManager = new ClockManager(this);

            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;

            graphics.PreferredDepthStencilFormat = SelectStencilMode();
        }

        public ClockManager ClockManager
        {
            get { return clockManager; }
        }

        public SoundManager SoundManager
        {
            get { return soundManager; }
        }

        private static DepthFormat SelectStencilMode()
        {
            // Check stencil formats
            GraphicsAdapter adapter = GraphicsAdapter.DefaultAdapter;
            SurfaceFormat format = adapter.CurrentDisplayMode.Format;
            if (adapter.CheckDepthStencilMatch(DeviceType.Hardware, format,
                format, DepthFormat.Depth24Stencil8))
                return DepthFormat.Depth24Stencil8;
            if (adapter.CheckDepthStencilMatch(DeviceType.Hardware, format,
                format, DepthFormat.Depth24Stencil8Single))
                return DepthFormat.Depth24Stencil8Single;
            if (adapter.CheckDepthStencilMatch(DeviceType.Hardware, format,
                format, DepthFormat.Depth24Stencil4))
                return DepthFormat.Depth24Stencil4;
            if (adapter.CheckDepthStencilMatch(DeviceType.Hardware, format,
                format, DepthFormat.Depth15Stencil1))
                return DepthFormat.Depth15Stencil1;
            throw new InvalidOperationException(
                "Could Not Find Stencil Buffer for Default Adapter");
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            ground = new GameObject();
            gameCamera = new Camera();
            random = new Random();
            levels = new Levels();
            
            barrierAnimationData = new BarrierAnimationData
                                       {
                                           barrierAlpha = GameConstants.BarrierAlphaNormal
                                       };

            inputManager = new InputManager(this);
            Components.Add(inputManager);
            soundManager = new SoundManager(this);
            animationManager = new AnimationManager(clockManager);

            skybox = new Skybox("SkyBoxes/Islands", Content);

            // create a lightsource that matches position of the sun in this skybox
            OrangeLightSource = new LightSource
            {
                Color = Color.Orange,
                Intensity = 0.8f,
                Direction = new Vector3(-2.8f, -1.3f, 0.0f)
            };

            // the lightsource that will add some light to the other sides of our objects
            BlueLightSource = new LightSource
            {
                Color = Color.PaleVioletRed,
                Intensity = 0.1f,
                Direction = new Vector3(+2.8f, -1.3f, 0.0f)
            };

            //Initialize fuel carrier
            fuelCarrier = new FuelCarrier();
            fuelCarrier.LoadContent(Content, GameConstants.ModelNameShip);
            fuelCarrier.lightSource1 = OrangeLightSource;
            fuelCarrier.lightSource2 = BlueLightSource;

            //Initialize barriermodels
            barrierModels[0] = Content.Load<Model>(GameConstants.ModelNameBarrierType1);
            barrierModels[1] = Content.Load<Model>(GameConstants.ModelNameBarrierType2);
            barrierModels[2] = Content.Load<Model>(GameConstants.ModelNameBarrierType3);

            fuelCells = new FuelCells();
            barriers  = new Barriers();

            PlaceFuelCellsAndBarriers();
   
            base.Initialize();
        }


        private void PlaceFuelCellsAndBarriers()
        {
            var levelGenerator = new LevelGenerator(this);
            levelGenerator.PlaceFuelCellsAndBarriers(fuelCells, barriers, levels.Current,
                                                     fuelCarrier.BoundingSphere.Radius);
        }


        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
            ground.Model = Content.Load<Model>("Models/ground");
            boundingSphereModel = Content.Load<Model>("Models/sphere1uR");
            font = Content.Load<SpriteFont>("Fonts/Arcade");
            countdownfont = Content.Load<SpriteFont>("Fonts/CountDown");

            hud = new Hud(Content.Load<Texture2D>("Textures/canister"),
                          Content.Load<Texture2D>("Textures/hudbackground"), 
                          Content.Load<Texture2D>("Textures/TimerBar"), 
                          animationManager);

            var rectSafeArea = GraphicsDevice.Viewport.TitleSafeArea;
            int hudHeight = rectSafeArea.Height/15;
            int hudWidth = rectSafeArea.Width/5;
            int timerHeight = hudHeight/3;
            var rect = new Rectangle(rectSafeArea.X + 10, rectSafeArea.Bottom - 10 - timerHeight, hudWidth, timerHeight);
            hud.TimeRemainingIndicatorArea = rect;
            rect.Height = hudHeight;
            rect.Y -= hudHeight+2;
            //hud.TimeRemainingIndicatorArea = hud.FuelCellsIndicatorArea;
            //hud.TimeRemainingIndicatorArea= Inflate(0, -hudHeight * 4 / 5);
            //hud.TimeRemainingIndicatorArea = new Rectangle(hud.FuelCellsIndicatorArea.X, hud.FuelCellsIndicatorArea.Y-hudHeight/5, hudWidth, hudHeight/5);
            //hud.TimeRemainingIndicatorArea.Offset(0, -hudHeight);
            hud.FuelCellsIndicatorArea = rect;

            // some work to get a shadow on the ground:
            shadowPlane = new Plane(Vector3.Up, 0f);
            // The shadow is based on the strongest light, and put slightly higher than the ground
            Vector3 shadowLightDir = -OrangeLightSource.Direction;
            shadow = Matrix.CreateShadow(shadowLightDir, shadowPlane) *
                     Matrix.CreateTranslation(Vector3.Up / 100f);

            soundManager.LoadAudioContent();

        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {

            // Allows the game to exit
            if (inputManager.IsExitPressed)
                this.Exit();

            clockManager.Update(gameTime);

            float aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio;

            if (currentGameState == GameState.Loading)
            {
                // player wants to start ? 
                if (inputManager.StartPressed) StartGame();
                if (inputManager.IsKeyPressed(Keys.F)) graphics.ToggleFullScreen();
                if (inputManager.IsKeyPressed(Keys.M)) GameConstants.ToggleMusic();
            }

            if (currentGameState == GameState.Running || 
                currentGameState == GameState.CountDown || 
                currentGameState == GameState.Won || 
                currentGameState == GameState.Lost)
            {
                GameIsRunningFast &= !gameTime.IsRunningSlowly;

                if (inputManager.IsKeyPressed(Keys.V)) gameCamera.NextPosition();

                // Allow game components to update 
                fuelCarrier.Update(inputManager, barriers, currentGameState, gameTime.ElapsedGameTime);
                gameCamera.Update(fuelCarrier.ForwardDirection, fuelCarrier.Position, aspectRatio);
                fuelCells.Update(fuelCarrier.BoundingSphere);
            }

            if (currentGameState == GameState.StartCountDown)
            {
                countDown = animationManager.StartCountDown(levels.CurrentLevel, countdownfont);
                currentGameState = GameState.CountDown;
            }
            if (currentGameState == GameState.CountDown)
            {
                countDown.Update(gameTime.ElapsedGameTime);
                // switch game to "running" in last second of countdown (the "GO!" part)
                if (countDown.Timer < TimeSpan.FromSeconds(1))
                    currentGameState = GameState.Running;
            }

            if (currentGameState == GameState.Running)
            {
#if DEBUG
                if (inputManager.IsKeyPressed(Keys.F9)) GameConstants.ToggleWireFrames();
                if (inputManager.IsKeyPressed(Keys.F10)) currentGameState = GameState.Lost;
                if (inputManager.IsKeyPressed(Keys.F11)) currentGameState = GameState.Won;
#endif

                // camera reacts to thrust when following fuelCarrier
                gameCamera.ChaseDistance = fuelCarrier.Thrust;

                // countdown needs update to allow it to remove the "GO!"
                countDown.Update(gameTime.ElapsedGameTime);

                // check for fuel pickups
                fuelCells.Update(fuelCarrier.BoundingSphere);
                retrievedFuelCells = fuelCells.CountRetrieved;
                if (retrievedFuelCells != oldRetrievedFuelCells)
                    hud.OnCanisterFound(retrievedFuelCells);
                oldRetrievedFuelCells = retrievedFuelCells;

                // check for gameover (won or lost)
                roundTimer -= gameTime.ElapsedGameTime;
                hud.SetTimeLeft((float)roundTimer.TotalSeconds);

                if (roundTimer < TimeSpan.Zero &&
                    retrievedFuelCells != levels.Current.NumOfFuelCells)
                    currentGameState = GameState.Lost;
                if (retrievedFuelCells == levels.Current.NumOfFuelCells)
                    currentGameState = GameState.Won;
            }

            if ((currentGameState == GameState.Won) || (currentGameState == GameState.Lost))
            {

                if (previousGameState != currentGameState)
                {
                    soundManager.StopMusic();
                    fuelCarrier.Thrust = 0f;
                    if (currentGameState == GameState.Lost)
                    {
                        soundManager.PlayGameLostSound();
                        animationManager.StartBarrierAnimation(barrierAnimationData);
                    }
                    if (currentGameState == GameState.Won)
                    {
                        gameCamera.Operation = CameraOperation.following;
                        animationManager.PlayGameWonAnimation(fuelCarrier);
                    }
                }

                // Reset the world for a new game
                if (inputManager.StartPressed)
                    ResetGame(currentGameState == GameState.Won);
            }

            previousGameState = currentGameState;
            base.Update(gameTime);
        }

        private void StartGame()
        {
            roundTimer = TimeSpan.FromSeconds(levels.Current.RoundTimeInSeconds);
            hud.Initialize(levels.Current);
            retrievedFuelCells = 0;
            fuelCarrier.Reset();
            currentGameState = GameState.StartCountDown;
            soundManager.StartMusic();
            GameIsRunningFast = true;
        }

        private void ResetGame(bool gotoNextLevel)
        {
            gameCamera.Operation = CameraOperation.chasing;
            animationManager.StopAllAnimations();
            soundManager.ResetSounds();
            fuelCarrier.Reset();

            barrierAnimationData.barrierAlpha = GameConstants.BarrierAlphaNormal;
            if (gotoNextLevel)
                levels.GoToNextLevel();
            PlaceFuelCellsAndBarriers();
            StartGame();
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(Color.Black);

            FPS = 1f / (float)gameTime.ElapsedGameTime.TotalSeconds;

            switch (currentGameState)
            {
                case GameState.Loading:
                    DrawSplashScreen();
                    break;
                case GameState.CountDown:
                case GameState.Running:
                    DrawGameplayScreen();
                    DrawCountDown();
                    break;
                case GameState.Won:
                    DrawWinOrLossScreen(GameConstants.StrLevelCompleted);
                    break;
                case GameState.Lost:
                    DrawWinOrLossScreen(GameConstants.StrGameLost);
                    break;
            }
            
            base.Draw(gameTime);
        }

        private void DrawCountDown()
        {
            countDown.Draw(spriteBatch, GraphicsDevice.Viewport.TitleSafeArea);
        }

        private void DrawWinOrLossScreen(string msg)
        {
            DrawGameplayScreen();

            Vector2 viewportSize = new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);

            //graphics.GraphicsDevice.Clear(Color.SteelBlue);

            Vector2 strPositionMsg = GetStrPositionCentered(msg, viewportSize, 0);
            Vector2 strPositionPlayAgain = GetStrPositionCentered(GameConstants.StrPlayAgain, viewportSize, 1);

            spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
            spriteBatch.DrawString(font, msg, strPositionMsg, Color.White);
            spriteBatch.DrawString(font, GameConstants.StrPlayAgain, strPositionPlayAgain, Color.LightGray);
            spriteBatch.End();

            //re-enable depth buffer after sprite batch disablement
//            GraphicsDevice.RenderState.DepthBufferEnable = true;
//            GraphicsDevice.RenderState.AlphaBlendEnable = false;
//            GraphicsDevice.RenderState.AlphaTestEnable = false;
//            GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
//            GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;

        }

        private Vector2 GetStrPositionCentered(string msg, Vector2 viewportSize, int lineOffset)
        {
            Vector2 size = font.MeasureString(msg);
            Vector2 strCenter = new Vector2(size.X / 2, size.Y / 2);

            float xOffsetText = (viewportSize.X / 2 - strCenter.X);
            float yOffsetText = (viewportSize.Y / 2 - strCenter.Y) + font.LineSpacing * lineOffset;
            return new Vector2(xOffsetText, yOffsetText);
        }

        private void DrawGameplayScreen()
        {

            // draw the skybox
            graphics.GraphicsDevice.RenderState.AlphaBlendEnable = false;
            graphics.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
            skybox.Draw(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix, gameCamera.Position);
            graphics.GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;


            // Set states to allow alphablending (a cheat to see thru the barriers)
            graphics.GraphicsDevice.RenderState.AlphaBlendEnable = true;
            graphics.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha; // source rgb * source alpha
            graphics.GraphicsDevice.RenderState.AlphaSourceBlend = Blend.One; // don't modify source alpha
            graphics.GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha; // dest rgb * (255 - source alpha)
            graphics.GraphicsDevice.RenderState.AlphaDestinationBlend = Blend.InverseSourceAlpha; // dest alpha * (255 - source alpha)
            graphics.GraphicsDevice.RenderState.BlendFunction = BlendFunction.Add; // add source and dest results

            DrawTerrain(ground.Model);

            fuelCells.ForEach(cell =>
                                  {
                                      DrawBoundingSphere(cell);
                                      cell.Draw(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix);
                                  });

            barriers.ForEach(b =>
                                 {
                                     DrawBoundingSphere(b);
                                     b.Draw(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix,
                                            barrierAnimationData.barrierAlpha);
                                 });

            fuelCarrier.Draw(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix);
            DrawBoundingSphere(fuelCarrier);

            if (GameIsRunningFast)
            {
                // switch to shadows now
                GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 0);
                
                GraphicsDevice.RenderState.StencilEnable = true;
                // Draw on screen if 0 is the stencil buffer value           
                GraphicsDevice.RenderState.ReferenceStencil = 0;
                GraphicsDevice.RenderState.StencilFunction = CompareFunction.Equal;
                // Increment the stencil buffer if we draw
                GraphicsDevice.RenderState.StencilPass = StencilOperation.Increment;
                // Setup alpha blending to make the shadow semi-transparent
                GraphicsDevice.RenderState.AlphaBlendEnable = true;
                GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
                GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

                // Draw the shadow without lighting
                fuelCells.ForEach(cell => cell.DrawShadow(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix, shadow));
                barriers.ForEach(barrier => barrier.DrawShadow(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix,
                                                               shadow));
                fuelCarrier.DrawShadow(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix, shadow);

                // Return render states to normal            
                // turn stencilling off
                GraphicsDevice.RenderState.StencilEnable = false;
                // turn alpha blending off
                GraphicsDevice.RenderState.AlphaBlendEnable = false;
            }

            DrawStats();
        }

        private void DrawSplashScreen()
        {
            Vector2[] positions = new Vector2[3];
            Vector2 viewportSize = new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);

            graphics.GraphicsDevice.Clear(Color.SteelBlue);

            positions[0] = GetStrPositionCentered(GameConstants.StrInstructions1, viewportSize, 0);
            positions[1] = GetStrPositionCentered(GameConstants.StrInstructions2, viewportSize, 1);
            positions[2] = GetStrPositionCentered(GameConstants.StrInstructions3, viewportSize, 2);

            spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
            spriteBatch.DrawString(font, GameConstants.StrInstructions1, positions[0], Color.White);
            spriteBatch.DrawString(font, GameConstants.StrInstructions2, positions[1], Color.LightGray);
            spriteBatch.DrawString(font, GameConstants.StrInstructions3, positions[2], Color.LightGray);
            spriteBatch.End();

            //re-enable depth buffer after sprite batch disablement
//            GraphicsDevice.RenderState.DepthBufferEnable = true;
//            GraphicsDevice.RenderState.AlphaBlendEnable = false;
//            GraphicsDevice.RenderState.AlphaTestEnable = false;
//            GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
//            GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
        }

        private void DrawStats()
        {
            string str1 = 
                GameConstants.StrLevel + (levels.CurrentLevel+1) + " " +
                GameConstants.StrTimeRemaining + (roundTimer.Seconds).ToString();
#if DEBUG
            str1 += " Steering: " + fuelCarrier.TurnImpulse.ToString("F4")
                + " Y: " + fuelCarrier.Height.ToString("F4")
                + " Thrust: " + fuelCarrier.Thrust.ToString("F4");
#endif

            //Calculate str1 position
            Rectangle rectSafeArea = GraphicsDevice.Viewport.TitleSafeArea;

            float xOffsetText = rectSafeArea.X;
            float yOffsetText = rectSafeArea.Y;

            Vector2 strPosition = new Vector2((int)xOffsetText + 10, (int)yOffsetText);

            string strFPS = "FPS: " + FPS.ToString("N0");
            Vector2 strFPSPosition = new Vector2(rectSafeArea.Width - 10 - font.MeasureString(strFPS).X, yOffsetText);

            spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
            spriteBatch.DrawString(font, str1, strPosition, Color.White);
            spriteBatch.DrawString(font, strFPS, strFPSPosition, Color.White);

            hud.Draw(spriteBatch);
            spriteBatch.End();

            //            //re-enable depth buffer after sprite batch disablement
            //            GraphicsDevice.RenderState.DepthBufferEnable = true;
            //            GraphicsDevice.RenderState.AlphaBlendEnable = false;
            //            GraphicsDevice.RenderState.AlphaTestEnable = false;
            //            GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
            //            GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
        }

        private void DrawBoundingSphere(GameObject gameObject)
        {
            if (GameConstants.DrawWireFrames)
            {
                GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
                gameObject.DrawBoundingSphere(gameCamera.ViewMatrix, gameCamera.ProjectionMatrix, boundingSphereModel);
                GraphicsDevice.RenderState.FillMode = FillMode.Solid;
            }
        }

        private void DrawTerrain(Model model)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;
                    effect.Alpha = GameConstants.TerrainAlpha;
                    effect.World = Matrix.Identity;
                    if (OrangeLightSource != null)
                        OrangeLightSource.ApplyLight(effect.DirectionalLight0);
                    effect.DirectionalLight0.DiffuseColor *= 0.5f; // weaken the light ??
                    // Use the matrices provided by the game camera
                    effect.View = gameCamera.ViewMatrix;
                    effect.Projection = gameCamera.ProjectionMatrix;
                }
                mesh.Draw();
            }
        }
    }
}
